/*
 * BlackLightning.PluginManager
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.Collections.Generic;
using BlackLightning.PluginLib;
using BlackLightning.PluginLib.Modules;
using BLLib.Configuration;

namespace BlackLightning {
	/// <summary>
	/// Manages all loaded plugins and acts as the interface between plugins/modules and the rest of Black Lightning.
	/// </summary>
	internal class PluginManager {
		// member data
		private BlackLightning             _BlackLightning; // the BlackLightning instance which owns this PluginManager
		private Dictionary<string, Plugin> _Plugins;        // a collection of all the loaded plugins
		private Module                     _SystemModule;   // the system module
		private OptionSet                  _Options;        // options pertaining to all plugins

		/// <summary>
		/// Creates a new PluginManager.
		/// </summary>
		/// <param name="BL">The BlackLightning instance which will own this PluginManager.</param>
		public PluginManager(BlackLightning BL) {
			// store our data
			_BlackLightning = BL;
			_Plugins        = new Dictionary<string, Plugin>();

			// grab our options
			_Options = _BlackLightning.Options.GetOrCreateOptionSet(BlackLightning.PluginsOptionSetName);

			// load up the system module
			_SystemModule = new Module(new ModuleProxy(new SystemModule(_BlackLightning)), MessageReceived, PacketReceived);
			_SystemModule.HandleMessage(new LoadedMessage(_BlackLightning.SalFile));

			// run through our options looking for modules to autoload
			foreach (OptionSet PluginOS in _Options.Children) {
				// this set represents a plugin
				string PluginName = PluginOS.Name;

				// run through each of its modules
				foreach (OptionSet ModuleOS in PluginOS.Children) {
					// this set represents a module
					string ModuleName = ModuleOS.Name;

					// find its autoload option
					Option O = ModuleOS.GetOption(Module.LoadAtStartupOptionName);
					if (O == null) {
						continue;
					}

					// check if it's enabled or not
					if (O.Value as string == true.ToString()) {
						// load the module
						Load(PluginName, ModuleName);
					}
				}
			}
		}

		/// <summary>
		/// Gets a collection of plugins currently loaded (maps names to the corresponding Plugin object).
		/// </summary>
		public Dictionary<string, Plugin> Plugins {
			get {
				return _Plugins;
			}
		}

		/// <summary>
		/// Finds a plugin by name.
		/// </summary>
		/// <param name="Name">The name of the plugin to find.</param>
		/// <returns>The plugin with the given name if it is loaded, null if it is not.</returns>
		public Plugin GetPlugin(string Name) {
			if (_Plugins.ContainsKey(Name)) {
				return _Plugins[Name];
			} else {
				return null;
			}
		}

		/// <summary>
		/// Gets a module by its name.
		/// </summary>
		/// <param name="Name">The name or full name of the module to find.</param>
		/// <returns>The module with the given name if it is loaded, null if it is not.</returns>
		/// <exception cref="ArgumentException">Thrown if more than one module from different plugins both have the given name.</exception>
		public Module GetModule(string Name) {
			// check if this is a full name
			int Index = Name.IndexOf(Module.FullnameSeparator);
			if (Index > 0 && Index < Name.Length-1-Module.FullnameSeparator.Length) {
				// split the full name into plugin and module name, and call the other version
				return GetModule(Name.Substring(0, Index), Name.Substring(Index+Module.FullnameSeparator.Length));
			}

			// run through our plugins and check for any modules with this name
			Module M = null;
			int Found = 0;
			foreach (Plugin P in _Plugins.Values) {
				M = P.GetModuleByName(Name);
				if (M != null) {
					++Found;
				}
			}

			// check what we found
			if (Found > 1) {
				throw new ArgumentException("The given module name '" + Name + "' is currently loaded in more than one plugin.  Unable to distinguish which is requested.");
			} else if (Found == 1) {
				return M;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Finds a module given its name and the name of its plugin.
		/// </summary>
		/// <param name="PluginName">The name of the plugin containing the module to find.</param>
		/// <param name="ModuleName">The name of the module to find.</param>
		/// <returns>The module with the given name from the given plugin if it is loaded, null if it is not.</returns>
		public Module GetModule(string PluginName, string ModuleName) {
			// find the plugin
			Plugin P = GetPlugin(PluginName);
			if (P == null) {
				return null;
			}

			// find the module in the plugin
			return P.GetModuleByName(ModuleName);
		}

		/// <summary>
		/// Loads the given plugin and all modules inside it.
		/// </summary>
		/// <param name="Name">The name of the plugin to load, or the full name of the module to load.</param>
		public void Load(string Name) {
			// if we have a module name separator in the name, call the other version with the module name
			int Index = Name.IndexOf(Module.FullnameSeparator);
			if (Index > 0 && Index < Name.Length-1-Module.FullnameSeparator.Length) {
				Load(Name.Substring(0, Index), Name.Substring(Index+Module.FullnameSeparator.Length));
				return;
			}

			// otherwise, we're just dealing with a plugin
			Load(Name, null);
		}

		/// <summary>
		/// Loads the given plugin and one or all of the modules inside it.
		/// </summary>
		/// <param name="PluginName">The name of the plugin to load.</param>
		/// <param name="ModuleName">The name of the module within the plugin to load, or null to load them all.</param>
		public void Load(string PluginName, string ModuleName) {
			try {
				// load the plugin
				Plugin P = null;
				if (_Plugins.ContainsKey(PluginName)) {
					if (ModuleName != null) {
						// this plugin is already loaded, use that
						P = _Plugins[PluginName];
					} else {
						// this plugin is already loaded, send an error
						_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "The plugin '" + PluginName + "' is already loaded."));
						return;
					}
				} else {
					// load the plugin
					P = new Plugin(PluginName);

					// wire its events to fire ours
					P.SendMessage += MessageReceived;
					P.SendPacket  += PacketReceived;

					// check if we already have options for this plugin or need new ones
					OptionSet PluginOptions = _Options.GetOrCreateOptionSet(P.Name);

					// initialize the plugin
					P.Initialize(PluginOptions, new LoadedMessage(_BlackLightning.SalFile));

					// add the plugin to our list
					_Plugins.Add(P.Name, P);

					// let the user know that all is well
					_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Successfully loaded plugin: " + P.Name));
				}

				// load the requested module(s)
				if (ModuleName == null) {
					// scan for a list of modules
					string[] FoundModules = P.ScanForModules();
					foreach (string FoundModule in FoundModules) {
						// load this module
						P.LoadModule(FoundModule, new LoadedMessage(_BlackLightning.SalFile));
						_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Successfully loaded module: " + P.Name + Module.FullnameSeparator + FoundModule));
					}
				} else {
					// load the requested module
					P.LoadModule(ModuleName, new LoadedMessage(_BlackLightning.SalFile));
					_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Successfully loaded module: " + P.Name + Module.FullnameSeparator + ModuleName));
				}
			} catch (LoadException x) {
				_BlackLightning.Dispatcher.SendPacket(new ExceptionPacket(x, true));
			} catch (Exception x) {
				_BlackLightning.Dispatcher.SendPacket(new ExceptionPacket(x));
			}
		}

		/// <summary>
		/// Unloads all plugins and all modules.  The PluginManager can no longer be used after this.
		/// </summary>
		public void Unload() {
			// run through each plugin and unload it
			Dictionary<string, Plugin> Temp = new Dictionary<string,Plugin>(_Plugins);
			foreach (Plugin P in Temp.Values) {
				Unload(P);
			}

			// unload the system plugin
			_SystemModule.HandleMessage(new UnloadedMessage());
			_SystemModule.Unload(MessageReceived, PacketReceived);
			_BlackLightning.PacketRegistry.Unregister(_SystemModule);
			_SystemModule = null;
		}

		/// <summary>
		/// Unloads a module or plugin.
		/// </summary>
		/// <param name="Name">The name of the module or plugin to unload.</param>
		public void Unload(string Name) {
			// if we have a module name separator in the name, call the other version
			int Index = Name.IndexOf(Module.FullnameSeparator);
			if (Index > 0 && Index < Name.Length-1-Module.FullnameSeparator.Length) {
				Unload(Name.Substring(0,Index), Name.Substring(Index+Module.FullnameSeparator.Length));
				return;
			}

			int ObjectsFound = 0;
			Plugin FoundPlugin = null;
			string FoundModule = null;

			// loop through each plugin
			foreach (Plugin P in _Plugins.Values) {
				// check if this plugin has a matching name
				if (P.Name == Name) {
					// we found a matching object
					++ObjectsFound;
					FoundPlugin = P;
				}

				// check if this plugin has a module with a matching name
				if (P.LoadedModules.Contains(Name)) {
					// we found a matching object
					++ObjectsFound;
					FoundModule = Name;
					FoundPlugin = P;
				}
			}

			// if we found more than one object, this won't work
			if (ObjectsFound > 1) {
				_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "More than one plugin/module was found with the name '" + Name + "', unsure which to unload."));
				return;
			} else if (ObjectsFound == 0) {
				_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "No plugins or modules were found with the name: " + Name));
				return;
			}

			// unload the object we found
			if (FoundModule != null) {
				Unload(FoundPlugin, FoundModule);
			} else if (FoundPlugin != null) {
				Unload(FoundPlugin);
			}
		}

		/// <summary>
		/// Unloads the given module from the given plugin.
		/// </summary>
		/// <param name="PluginName">The name of the plugin to unload the module from.</param>
		/// <param name="ModuleName">The name of the module to unload from the given plugin.</param>
		public void Unload(string PluginName, string ModuleName) {
			// find the plugin
			Plugin P;
			try {
				P = _Plugins[PluginName];
			} catch (KeyNotFoundException) {
				_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Plugin '" + PluginName + "' is not currently loaded."));
				return;
			}

			// unload the module
			Unload(P, ModuleName);
		}

		/// <summary>
		/// Unloads the given plugin and all its modules.
		/// </summary>
		/// <param name="Plugin">The plugin to unload.</param>
		public void Unload(Plugin Plugin) {
			// unload any modules that are loaded
			foreach (string ModuleName in Plugin.LoadedModules) {
				Unload(Plugin, ModuleName, false);
			}

			// remove the plugin from our list
			_Plugins.Remove(Plugin.Name);

			// unload the plugin
			try {
				// store the name for displaying afterwards
				string Name = Plugin.Name;

				// unload the plugin
				Plugin.Unload(MessageReceived, PacketReceived);

				// remove all the plugin's timers
				_BlackLightning.Timekeeper.Timers.RemoveAll((new Timer.OwnerMatches(Plugin)).Check);

				// unregister all of the plugin's packet registrations
				_BlackLightning.PacketRegistry.Unregister(Plugin);

				// send a success message
				_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Successfully unloaded plugin: " + Name));
			} catch (Exception x) {
				_BlackLightning.Dispatcher.SendPacket(new ExceptionPacket(x));
			}
		}

		/// <summary>
		/// Unloads the given module from the given plugin.
		/// </summary>
		/// <param name="Plugin">The plugin to unload a module from.</param>
		/// <param name="ModuleName">The module to unload from the plugin.</param>
		public void Unload(Plugin Plugin, string ModuleName) {
			Unload(Plugin, ModuleName, true);
		}

		/// <summary>
		/// Unloads the given module from the given plugin.
		/// </summary>
		/// <param name="Plugin">The plugin to unload a module from.</param>
		/// <param name="ModuleName">The module to unload from the plugin.</param>
		/// <param name="CheckForLastModule">Whether or not the plugin should also be unloaded if it contains no more modules after the given one.</param>
		public void Unload(Plugin Plugin, string ModuleName, bool CheckForLastModule) {
			try {
				// unload the module (store its name first)
				Module M = Plugin.GetModuleByName(ModuleName);
				if (M == null) {
					_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "No module named '" + ModuleName + "' currently loaded in plugin: " + Plugin.Name));
					return;
				}

				// unload the module
				Plugin.UnloadModule(ModuleName);

				// remove all the module's timers
				_BlackLightning.Timekeeper.Timers.RemoveAll((new Timer.OwnerMatches(M)).Check);

				// unregister all of their packet registrations
				_BlackLightning.PacketRegistry.Unregister(M);

				// let the user know
				_BlackLightning.Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Successfully unloaded module: " + Plugin.Name + Module.FullnameSeparator + ModuleName));
			} catch (Exception x) {
				_BlackLightning.Dispatcher.SendPacket(new ExceptionPacket(x));
			}

			// if that was the last module in the plugin, unload the plugin as well
			if (CheckForLastModule && (Plugin.LoadedModules.Count == 0)) {
				Unload(Plugin);
			}
		}
		
		/// <summary>
		/// Called when a Message is received from a plugin or a module.
		/// </summary>
		/// <param name="Sender">The plugin or module that sent the Message.</param>
		/// <param name="e">Arguments about the Message received.</param>
		private void MessageReceived(object Sender, SendMessageEventArgs e) {
			// check which type of message this is
			if (e.Message is RegisterPacketMessage) {
				// register the given arguments with the packet registry
				RegisterPacketMessage Msg = e.Message as RegisterPacketMessage;
				_BlackLightning.PacketRegistry.Register(Sender as IPacketHandler, Msg.Filter, Msg.Priority, Msg.NewThread);
			} else if (e.Message is UnregisterPacketMessage) {
				// unregister the given filter from the packet registry
				UnregisterPacketMessage Msg = e.Message as UnregisterPacketMessage;
				_BlackLightning.PacketRegistry.Unregister(Sender as IPacketHandler, Msg.Filter);
			} else if (e.Message is ShowOptionsMessage) {
				// have Black Lightning show the options dialog for plugins
				_BlackLightning.ShowPluginOptions();
			} else if (e.Message is RegisterTimerMessage) {
				// create a new timer in the Timekeeper
				RegisterTimerMessage Msg = e.Message as RegisterTimerMessage;
				_BlackLightning.Timekeeper.Timers.Add(new Timer(Sender as IMessageHandler, Msg.ID, Msg.Start, Msg.ExpireCount, Msg.Interval));
			} else if (e.Message is UnregisterTimerMessage) {
				// remove the given timer from the Timekeeper
				UnregisterTimerMessage Msg = e.Message as UnregisterTimerMessage;
				_BlackLightning.Timekeeper.Timers.RemoveAll((new Timer.IDMatches(Sender as IMessageHandler, Msg.ID)).Check);
			}
		}

		/// <summary>
		/// Called when a Packet is received from a plugin or a module.
		/// </summary>
		/// <param name="Sender">The plugin or module that sent the Packet.</param>
		/// <param name="e">Arguments about the Packet received.</param>
		private void PacketReceived(object Sender, SendPacketEventArgs e) {
			// hand the packet off to the dispatcher
			_BlackLightning.Dispatcher.SendPacket(e.Packet);
		}
	}
}
